/**
 * Created on Apr 22, 2007
 */
package jvs.vfs;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import x.java.io.File;
import x.java.net.VStreamHandlerFactory;
import android.util.Log;

/**
 * @author qiangli
 * 
 */
public class FileSystem {
	private static FileSystem fs = new FileSystem();

	public static final String[] JVS_FOLDERS = { "/", "/android/",
			"/application/", "/my/", "/system/", "/temp/" };

	public static final String JVS_ROOTFS = "jvs.rootfs";

	public static final boolean DEBUG = false;

	private static final String LOG_TAG = "JVS FileSystem";

	private static String[] buildPaths(String s) {
		s = removeV(s);
		String[] sa = s.split("/");
		if (sa.length == 0) {
			return new String[] { "/" };
		}
		String[] pa = new String[sa.length];
		for (int i = 1; i <= sa.length; i++) {
			StringBuffer sb = new StringBuffer();
			for (int j = 0; j < i; j++) {
				sb.append(sa[j] + "/");
			}
			String f = sb.toString();
			int len = f.length();
			pa[pa.length - i] = (len == 1 ? "/" : f.substring(0, len - 1));
		}
		return pa;
	}

	public static String encodePath(String s) {
		try {
			return URIEncoder.encodeURI(s);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return s;
	}

	/**
	 * get default file system
	 * 
	 * @return
	 */
	public static FileSystem getFileSystem() {
		return fs;
	}

	/**
	 * @throws NullPointerException
	 *             if path is null
	 */
	private static boolean hasV(String path) {
		if (path.startsWith("v:") || path.startsWith("V:")) {
			return true;
		}
		return false;
	}

	/**
	 * 
	 * @param path
	 * @return path with virtual drive letter v removed
	 */
	private static String removeV(String path) {
		if (path == null || path.length() < 2) {
			return path;
		}
		if (hasV(path)) {
			return path.substring(2);
		}
		return path;
	}

	private ArrayList<URL> bootClassPath = null;

	//private java.io.File tempDirectory;

	private Properties contentTypes = new Properties();

	//private File userDirectory;

	private Properties fsmap = new Properties();

	private LinkedHashMap<String, Fstab.Entry> mtab = new LinkedHashMap<String, Fstab.Entry>();

	private HashMap<String, Class<?>> providers = new HashMap<String, Class<?>>();

	private File root = null;

	private long uptime = java.lang.System.currentTimeMillis();

	private final String version = "JVS VFS Version 0.1a by Li, Qiang";

	private FileSystem() {
	}

	public synchronized void boot() {
		String uri = java.lang.System.getProperty(JVS_ROOTFS);
		if (uri == null) {
			uri = "jfs:resource:/boot/root.xml!/";
			java.lang.System.setProperty(JVS_ROOTFS, uri);
			if (DEBUG) {
				debug("Filesystem property jvs.rootfs not set (-Djvs.rootfs=<uri>), using default: "
								+ uri);
			}
		}
		boot(uri);
	}

	/**
	 * 
	 * @param uri
	 *            root fs
	 * @param root
	 *            custom root folder
	 */
	public synchronized void boot(String uri) {
		try {
			// set built-in handlers
			java.lang.System.getProperties().put("java.protocol.handler.pkgs",
					"x.java.net.protocol");
			URL.setURLStreamHandlerFactory(new VStreamHandlerFactory());
			//
			fsmap.put("file", jvs.vfs.local.LocalFileImpl.class.getName());
			fsmap.put("jfs", jvs.vfs.dom.JfsFileImpl.class.getName());
			fsmap.put("resource", jvs.vfs.resource.ResourceFileImpl.class
					.getName());
			fsmap.put("proc", jvs.vfs.proc.ProcFileImpl.class.getName());
			fsmap.put("link", jvs.vfs.link.LinkFileImpl.class.getName());
			fsmap.put("arc", jvs.vfs.arc.ArchiveFileImpl.class.getName());
			//
			fsmap.put("url", jvs.vfs.resource.UrlFileImpl.class.getName());
			fsmap.put("ftp", jvs.vfs.ftp.FtpFileImpl.class.getName());
			//
			// boot root fs
			//
			mount("/", new URI(uri), "rw");
			//
			loadContentTypes();
		} catch (Exception e) {
			error(e);
		}
	}

	private void error(Exception e) {
		e.printStackTrace();
	}

	/**
	 * 
	 * @param file
	 * @return true if filesystem is readable; false otherwise
	 */
	public boolean canRead(String path) {
		Fstab.Entry fe = findMountpoint(path);
		return fe != null && fe.canRead();
	}

	/**
	 * 
	 * @param path
	 * @return true if filesystem is writable; false otherwise
	 */
	public boolean canWrite(String path) {
		Fstab.Entry fe = findMountpoint(path);
		return fe != null && fe.canWrite();
	}

	private void changeRoot(File root) {
		this.root = root;
	}

	public void changeRoot(String root) {
		changeRoot(new File(root));
	}

	/**
	 * @param cls
	 * @param uri
	 * @return
	 * @throws Exception
	 */
	private IFile createIFile(Class<?> cls, URI uri) throws Exception {
		Class<?>[] parameterTypes = new Class[] { URI.class };
		Constructor<?> constructor = cls.getConstructor(parameterTypes);
		Object[] initargs = new Object[] { uri };
		Object obj = constructor.newInstance(initargs);
		return (IFile) obj;
	}

	synchronized public IFile createIFile(String path) throws Exception {
		URI uri = resolve(path);
		IFile ifile = createIFile(uri);

		if (DEBUG) {
			debug("created path: " + path + " uri: " + uri + "  ifile: " + ifile);
		}	
		// check auto mount, only support native file
		if (ifile.isFile() && ifile.isNative()
				&& fs.isMountPoint(path) == false) {
			return create(ifile, path);
		}
		return ifile;
	}

	public IFile createIFile(URI uri) throws Exception {
//		if (DEBUG) {
//			debug("createFile uri: " + uri);
//		}
		//
		IFile ifile = null;
		String scheme = uri.getScheme();
		//
		if (scheme.equals("file")) {
			ifile = new jvs.vfs.local.LocalFileImpl(uri);
		} else if (scheme.equals("jfs")) {
			ifile = new jvs.vfs.dom.JfsFileImpl(uri);
		} else if (scheme.equals("resource")) {
			ifile = new jvs.vfs.resource.ResourceFileImpl(uri);
		} else if (scheme.equals("link")) {
			ifile = new jvs.vfs.link.LinkFileImpl(uri);
		} else if (scheme.equals("arc")) {
			ifile = new jvs.vfs.arc.ArchiveFileImpl(uri);
		} else {
			Class<?> cls = findProvider(scheme);
			if (cls == null) {
				throw new Exception("No providers found for " + scheme);
			}
			ifile = createIFile(cls, uri);
		}
		return ifile;
	}

	/**
	 * Auto mount .zip, .ln
	 * 
	 * @param ifile
	 * @param path
	 * @return
	 */
	private IFile create(IFile ifile, String path) {
		try {
			if (DEBUG) {
				debug("(auto mount) create ifile: " +ifile + " path: " +path);
			}
			if (path.endsWith(File.LINK_EXT)) {
				// format: uri [option]
				String c = ifile.getContent();
				String[] sa = c.split("[ \t\r\n]", 2);
				String uri = sa[0];
				String opt = sa.length > 1 ? sa[1] : "r";
				fs.mount(path, new URI("link:" + uri), opt);
				return fs.createIFile(path);
			} else if (isArc(path)) {
				// arc:/realpath!/
				final String realpath = ifile.toRealURI().getPath();
				fs.mount(path, new URI("arc:" + realpath + "!/"), "r");
				return fs.createIFile(path);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ifile;
	}

	private boolean isArc(String path) {
		// currently supported arc extensions
		final String[] exts = new String[] { ".apk", ".zip", ".jar", ".war" };
		final int len = exts.length;
		for (int i = 0; i < len; i++) {
			if (path.endsWith(exts[i])) {
				return true;
			}
		}
		return false;
	}

	public String fileExt(String name) {
		int idx = name.lastIndexOf(".");
		if (idx > 0) {
			return name.substring(idx + 1).toLowerCase();
		}
		return null;
	}

	private Fstab.Entry findMountpoint(String path) {
		String[] ps = buildPaths(path);
		for (int i = 0; i < ps.length; i++) {
			Fstab.Entry fe = (Fstab.Entry) mtab.get(ps[i]);
			if (fe != null) {
				return fe;
			}
		}
		return null;
	}

	private Class<?> findProvider(String scheme) {
		Class<?> provider = providers.get(scheme);
		if (provider != null) {
			return provider;
		}
		String cname = fsmap.getProperty(scheme);
		try {
			ClassLoader cl = getClass().getClassLoader();
			Class<?> cls = cl.loadClass(cname);
			providers.put(scheme, cls);
			return cls;
		} catch (ClassNotFoundException e) {
			// e.printStackTrace();
		} catch (Exception e) {
			// e.printStackTrace();
			return null;
		}

		try {
			ClassLoader cl = getVfsClassLoader();
			Class<?> cls = cl.loadClass(cname);
			providers.put(scheme, cls);
			return cls;
		} catch (ClassNotFoundException e) {
		} catch (Exception e) {
			// e.printStackTrace();
			return null;
		}
		return null;
	}

	public List<URL> getBootClassPath() throws Exception {
		if (bootClassPath == null) {
			File blib = new File("/boot/lib");
			bootClassPath = new ArrayList<URL>();
			getClassPath(blib, (List<URL>) bootClassPath);
		}
		return bootClassPath;
	}

	public java.io.File getTempDirectory() {
		return new File("v:/temp/");
	}

	public List<URL> getClassPath() {
		// <ROOT>/classes/
		// <ROOT>/lib/*.[zip|jar]
		List<URL> list = new ArrayList<URL>();
		try {
			// list.addAll(getBootClassPath()); //
			// list.add(new File(root,"classes/").toURL());
			getClassPath(new File(root, "lib"), list);
			// list.add(new File(home, "classes/").toURL());
			// getClassPath(new File(home, "lib"), list);
		} catch (Exception e) {
			e.printStackTrace();
		}
		//Log.log(Log.DEBUG, this, "classpath: " + list);
		return list;
	}

	private void getClassPath(java.io.File file, List<URL> list)
			throws Exception {
		try {
			java.io.File[] files = file.listFiles();
			if (files == null || files.length == 0) {
				return;
			}
			for (int i = 0; i < files.length; i++) {
				if (files[i].isDirectory()) {
					getClassPath(files[i], list);
				} else if (files[i].isFile()) {
					String path = files[i].getPath();
					if (path.endsWith(".jar") || path.endsWith(".zip")) {
						list.add(files[i].toURI().toURL());
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public Properties getContentTypes() {
		return contentTypes;
	}

	public Properties getFsmap() {
		return fsmap;
	}

	public Map<String, Fstab.Entry> getMountpoints() {
		return mtab;
	}

	public File getRoot() {
		return root;
	}

	public long getUptime() {
		return uptime;
	}

	public String getVersion() {
		return version;
	}

	public ClassLoader getVfsClassLoader() {
		List<URL> list = getClassPath();
		//Log.log(Log.TRACE, this, list);
		ClassLoader cl = new VfsClassLoader(list, getClass().getClassLoader());
		return cl;
	}

	public String guessType(java.io.File file) {
		if (file.isDirectory()) {
			return null;
		}
		String type = null;
		if (file instanceof x.java.io.File) {
			x.java.io.File xf = (x.java.io.File) file;
			type = xf.getContentType();
			if (type != null) {
				return type;
			}
		}
		return guessType(file.getName());
	}

	public String guessType(String name) {
		String type = "application/octet-stream";
		String ext = fileExt(name);
		if (ext == null) {
			return type;
		}
		if (contentTypes != null) {
			type = contentTypes.getProperty(ext, type);
		}
		return type;
	}

	public boolean hasProvider(String scheme) {
		return providers.get(scheme) != null;
	}

	public boolean isMountPoint(String path) {
		return (mtab.get(path) != null);
	}

	public boolean isRoot(String p) {
		return p.equals("/") || p.equals("v:/");
	}

	public java.io.File[] listRoots() {
		return new x.java.io.File[] { root };
	}

	private void loadContentTypes() {
		try {
			InputStream is = getClass().getResourceAsStream(
					"/jvs/vfs/content-types.prp");
			loadContentTypes(is);
			is.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void loadContentTypes(InputStream is) {
		try {
			contentTypes.load(is);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void loadContentTypes(String file) {
		try {
			x.java.io.File f = new x.java.io.File(file);
			if (f.exists()) {
				InputStream is = f.getInputStream();
				contentTypes.load(is);
				is.close();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public boolean mkdirs(String path) {
		File f = new File(path);
		return (!f.exists() && f.mkdirs());
	}

	private synchronized void mount(Fstab.Entry fe) {
		if (DEBUG) {
			debug("mount: " + fe);
		}
		String mp = fe.getMountpoint();
		// remove v:/
		if (mp.startsWith("v:/")) {
			mp = mp.substring(2);
		}
		// remove trailing /
		if (mp.length() > 1 && mp.endsWith("/")) {
			mp = mp.substring(0, mp.length() - 1);
		}
		fe.setMountpoint(mp);
		//
		URI uri = fe.getUri();
		// pseudo scheme env:
		if (uri.getScheme().equals("env")) {
			String env = java.lang.System.getProperty(uri.toString().substring(
					4).trim());
			//Log.log(Log.DEBUG, this, "uri: " + uri + " env:" + env);
			if (env == null) {
				//Log.log(Log.INFO, this, "env not found for uri: " + uri);
				return;
			}
			int idx = env.indexOf(":");
			if (idx == -1 || !hasProvider(env.substring(0, idx))) { // file:
				java.io.File file = new java.io.File(env);
				uri = file.toURI();
			} else {
				try {
					uri = new URI(encodePath(env));
				} catch (URISyntaxException e) {
					e.printStackTrace();
				}
			}
		}
		uri = uri.normalize();
		// resolve local uri ie. v:/
		try {
			fe.setUri(resolve(uri));
		} catch (Exception e) {
			e.printStackTrace();
			return;
		}
		//
		Fstab.Entry oldfe = (Fstab.Entry) mtab.put(mp, fe);
		if (test(mp)) {
			if (oldfe != null) {
				if (DEBUG) {
					debug("unmounted: " + oldfe);
				}
			}
			if (DEBUG) {
				debug("mounted: " + fe);
			}
			if (mp.equals("/")) {
				changeRoot(new File("/"));
			}
		} else {
			fe = (Fstab.Entry) mtab.remove(mp);
			if (DEBUG) {
				debug("mount failed: " + mp + " " + fe.getUri());
			}
			// restore
			if (oldfe != null) {
				mtab.put(mp, oldfe);
			}
			throw new RuntimeException("failed to mount: " + mp + " "
					+ fe.getUri());
		}
	}

	private void debug(String s) {
		Log.d(LOG_TAG, s);
	}

	public void mount(String base, URI uri, String options) {
		Fstab.Entry fe = new Fstab.Entry();
		fe.setMountpoint(base);
		fe.setUri(uri);
		fe.setOptions(options);
		//
		mount(fe);
	}

	public void mountAll(String c) {
		//Log.log(Log.INFO, this, c);

		StringReader sr = new StringReader(c);
		Fstab fst = Fstab.parse(sr);

		for (Iterator<Fstab.Entry> it = fst.iterator(); it.hasNext();) {
			Fstab.Entry fe = (Fstab.Entry) it.next();
			File f = new File(fe.getMountpoint());
			if (!f.exists()) {
				if (!f.mkdirs()) {
					continue;
				}
			}
			mount(fe);
		}
	}

	public java.io.File nativeFile(File file) throws IOException {
		x.java.io.File vf = (x.java.io.File) file;
		URI uri = vf.getNativeURI();
		java.io.File nf = new java.io.File(URLDecoder.decode(uri.getPath(),
				"UTF-8"));
		return nf;
	}

	public void registerProvider(String scheme, String name) {
		fsmap.put(scheme, name);
	}

	/**
	 * Resolve virtual path to a real URI
	 */
	public URI resolve(String path) {
		//if (DEBUG) {
		//	debug("resolve path: " + path);
		//}
		String[] ps = buildPaths(path);
		for (int i = 0; i < ps.length; i++) {
			Fstab.Entry fe = (Fstab.Entry) mtab.get(ps[i]);
			if (fe != null) {
				String base = fe.getUri().toString();
				path = removeV(path);
				String relpath = path.substring(ps[i].length());
				URI uri = null;
				try {
					if (relpath.length() == 0) {
						uri = fe.getUri();
					} else {
						base = (base.endsWith("/") ? base.substring(0, base
								.length() - 1) : base);
						relpath = (relpath.startsWith("/") ? relpath
								.substring(1) : relpath);
						uri = new URI(base + "/" + encodePath(relpath))
								.normalize();
					}
				} catch (URISyntaxException e) {
					e.printStackTrace();
				}
				//Log.log(Log.TRACE, this, "base: " + base + " ps: " + ps[i]
				//		+ " relpath: " + relpath + " uri: " + uri);
				return uri;
			}
		}
		throw new IllegalArgumentException("No mounted filesystem found for: "
				+ path);
	}

	/**
	 * Find the real uri if virtual
	 */
	private URI resolve(URI uri) throws URISyntaxException {
		//if (DEBUG) {
		//	debug("resolve uri: " + uri);
		//}
		if (uri.getScheme().equals("v")) {
			return resolve(uri.getPath());
		} else {
			return uri;
		}
	}

	public void setContentTypes(Properties contentTypes) {
		this.contentTypes = contentTypes;
	}

	public File getUserDirectory() {
		return new File("v:/my/");
	}

	private boolean test(String path) {
		return true; //
//		try {
//			File f = new File(path);
//			return f.exists();
//			// return true;
//		} catch (Exception e) {
//			e.printStackTrace();
//		}
//		return false;
	}

	public void unmount(String base) {
		try {
			if (base.startsWith("v:/")) {
				base = base.substring(2);
			}
			Fstab.Entry fe = (Fstab.Entry) mtab.remove(base);
			//
			if (DEBUG) {
				debug("unmounted: " + base + " " + fe.getUri());
			}
		} catch (Exception e) {
			error(e);
		}
	}

	private Object context;

	public Object getContext() {
		return context;
	}

	public void setContext(Object context) {
		this.context = context;
	}
}
